﻿'use strict';

var hasOwn = Object.prototype.hasOwnProperty;

function noop() {
    return '';
}

function getStack(context) {
    return context.$$layoutStack || (
		context.$$layoutStack = []
);
}

function applyStack(context) {
    var stack = getStack(context);
    
    while (stack.length) {
        stack.shift()(context);
    }
}

function getActions(context) {
    return context.$$layoutActions || (
		context.$$layoutActions = {}
);
}

function getActionsByName(context, name) {
    var actions = getActions(context);
    
    return actions[name] || (
		actions[name] = []
);
}

function applyAction(val, action) {
    var context = this;
    
    function fn() {
        return action.fn(context, action.options);
    }
    
    switch (action.mode) {
        case 'append': {
            return val + fn();
        }

        case 'prepend': {
            return fn() + val;
        }

        case 'replace': {
            return fn();
        }

        default: {
            return val;
        }
    }
}

function mixin(target) {
    var arg, key,
        len = arguments.length,
        i = 1;
    
    for (; i < len; i++) {
        arg = arguments[i];
        
        if (!arg) {
            continue;
        }
        
        for (key in arg) {
            // istanbul ignore else
            if (hasOwn.call(arg, key)) {
                target[key] = arg[key];
            }
        }
    }
    
    return target;
}

/**
 * Generates an object of layout helpers.
 *
 * @type {Function}
 * @param {Object} handlebars Handlebars instance.
 * @return {Object} Object of helpers.
 */
function layouts(handlebars) {
    var helpers = {
        /**
		 * @method extend
		 * @param {String} name
		 * @param {?Object} customContext
		 * @param {Object} options
		 * @param {Function(Object)} options.fn
		 * @param {Object} options.hash
		 * @return {String} Rendered partial.
		 */
		extend: function (name, customContext, options) {
            // Make `customContext` optional
            if (arguments.length < 3) {
                options = customContext;
                customContext = null;
            }
            
            options = options || {};
            
            var fn = options.fn || noop,
                context = mixin({}, this, customContext, options.hash),
                data = handlebars.createFrame(options.data),
                template = handlebars.partials[name];
            
            // Partial template required
            if (template == null) {
                throw new Error('Missing partial: \'' + name + '\'');
            }
            
            // Compile partial, if needed
            if (typeof template !== 'function') {
                template = handlebars.compile(template);
            }
            
            // Add overrides to stack
            getStack(context).push(fn);
            
            // Render partial
            return template(context, { data: data });
        },
        
        /**
		 * @method embed
		 * @param {String} name
		 * @param {?Object} customContext
		 * @param {Object} options
		 * @param {Function(Object)} options.fn
		 * @param {Object} options.hash
		 * @return {String} Rendered partial.
		 */
		embed: function () {
            var context = mixin({}, this || {});
            
            // Reset context
            context.$$layoutStack = null;
            context.$$layoutActions = null;
            
            // Extend
            return helpers.extend.apply(context, arguments);
        },
        
        /**
		 * @method block
		 * @param {String} name
		 * @param {Object} options
		 * @param {Function(Object)} options.fn
		 * @return {String} Modified block content.
		 */
		block: function (name, options) {
            options = options || {};
            
            var fn = options.fn || noop,
                data = handlebars.createFrame(options.data),
                context = this || {};
            
            applyStack(context);
            
            return getActionsByName(context, name).reduce(
                applyAction.bind(context),
				fn(context, { data: data })
            );
        },
        
        /**
		 * @method content
		 * @param {String} name
		 * @param {Object} options
		 * @param {Function(Object)} options.fn
		 * @param {Object} options.hash
		 * @param {String} options.hash.mode
		 * @return {String} Always empty.
		 */
		content: function (name, options) {
            options = options || {};
            
            var fn = options.fn,
                data = handlebars.createFrame(options.data),
                hash = options.hash || {},
                mode = hash.mode || 'append',
                context = this || {};
            
            applyStack(context);
            
            // Getter
            if (!fn) {
                return name in getActions(context);
            }
            
            // Setter
            getActionsByName(context, name).push({
                options: { data: data },
                mode: mode.toLowerCase(),
                fn: fn
            });
        }
    };
    
    return helpers;
}

/**
 * Registers layout helpers on a Handlebars instance.
 *
 * @method register
 * @param {Object} handlebars Handlebars instance.
 * @return {Object} Object of helpers.
 * @static
 */
layouts.register = function (handlebars) {
    var helpers = layouts(handlebars);
    
    handlebars.registerHelper(helpers);
    
    return helpers;
};

module.exports = layouts;
